/*
 * Copyright (c) 2012, the Dart project authors.
 * 
 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.dart.tools.ui;

import com.google.dart.engine.utilities.source.SourceRange;
import com.google.dart.tools.core.model.DartModelException;
import com.google.dart.tools.core.model.SourceReference;
import com.google.dart.tools.core.utilities.io.FileUtilities;
import com.google.dart.tools.ui.internal.viewsupport.IProblemChangedListener;
import com.google.dart.tools.ui.internal.viewsupport.ImageDescriptorRegistry;
import com.google.dart.tools.ui.internal.viewsupport.ImageImageDescriptor;

import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ILightweightLabelDecorator;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.ui.texteditor.MarkerAnnotation;

import java.util.Iterator;

/**
 * LabelDecorator that decorates an element's image with error and warning overlays that represent
 * the severity of markers attached to the element's underlying resource. To see a problem
 * decoration for a marker, the marker needs to be a subtype of <code>IMarker.PROBLEM</code>.
 * <p>
 * <b>Important</b>: Although this decorator implements ILightweightLabelDecorator, do not
 * contribute this class as a decorator to the <code>org.eclipse.ui.decorators</code> extension.
 * Only use this class in your own views and label providers. Provisional API: This class/interface
 * is part of an interim API that is still under development and expected to change significantly
 * before reaching stability. It is being made available at this early stage to solicit feedback
 * from pioneering adopters on the understanding that any code that uses this API will almost
 * certainly be broken (repeatedly) as the API evolves.
 */
public class ProblemsLabelDecorator implements ILabelDecorator, ILightweightLabelDecorator {

  /**
   * This is a special <code>LabelProviderChangedEvent</code> carrying additional information
   * whether the event origins from a maker change.
   * <p>
   * <code>ProblemsLabelChangedEvent</code>s are only generated by <code>
   * ProblemsLabelDecorator</code>s.
   * </p>
   */
  public static class ProblemsLabelChangedEvent extends LabelProviderChangedEvent {

    private static final long serialVersionUID = 1L;

    private final boolean fMarkerChange;

    /**
     * Note: This constructor is for internal use only. Clients should not call this constructor.
     * 
     * @param eventSource the base label provider
     * @param changedResource the changed resources
     * @param isMarkerChange <code>true</code> if the change is a marker change; otherwise
     *          <code>false</code>
     */
    public ProblemsLabelChangedEvent(IBaseLabelProvider eventSource, IResource[] changedResource,
        boolean isMarkerChange) {
      super(eventSource, changedResource);
      fMarkerChange = isMarkerChange;
    }

    /**
     * Returns whether this event origins from marker changes. If <code>false</code> an annotation
     * model change is the origin. In this case viewers not displaying working copies can ignore
     * these events.
     * 
     * @return if this event origins from a marker change.
     */
    public boolean isMarkerChange() {
      return fMarkerChange;
    }

  }

  private static final int ERRORTICK_WARNING = DartElementImageDescriptor.WARNING;
  private static final int ERRORTICK_ERROR = DartElementImageDescriptor.ERROR;
  private static final int LINKED = DartElementImageDescriptor.LINKED;

  private ImageDescriptorRegistry fRegistry;
  private boolean fUseNewRegistry = false;
  private IProblemChangedListener fProblemChangedListener;

  private ListenerList fListeners;
  private SourceRange fCachedRange;

  /**
   * Creates a new <code>ProblemsLabelDecorator</code>.
   */
  public ProblemsLabelDecorator() {
    this(null);
    fUseNewRegistry = true;
  }

  /**
   * Note: This constructor is for internal use only. Clients should not call this constructor.
   * 
   * @param registry The registry to use or <code>null</code> to use the JavaScript plugin's image
   *          registry
   */
  public ProblemsLabelDecorator(ImageDescriptorRegistry registry) {
    fRegistry = registry;
    fProblemChangedListener = null;
  }

  /*
   * (non-Javadoc)
   * 
   * @see IBaseLabelProvider#addListener(ILabelProviderListener)
   */
  @Override
  public void addListener(ILabelProviderListener listener) {
    if (fListeners == null) {
      fListeners = new ListenerList();
    }
    fListeners.add(listener);
    if (fProblemChangedListener == null) {
      fProblemChangedListener = new IProblemChangedListener() {
        @Override
        public void problemsChanged(IResource[] changedResources, boolean isMarkerChange) {
          fireProblemsChanged(changedResources, isMarkerChange);
        }
      };
      DartToolsPlugin.getDefault().getProblemMarkerManager().addListener(fProblemChangedListener);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang .Object,
   * org.eclipse.jface.viewers.IDecoration)
   */
  @Override
  public void decorate(Object element, IDecoration decoration) {
    int adornmentFlags = computeAdornmentFlags(element);
    if (adornmentFlags == ERRORTICK_ERROR) {
      decoration.addOverlay(DartPluginImages.DESC_OVR_ERROR);
    } else if (adornmentFlags == ERRORTICK_WARNING) {
      decoration.addOverlay(DartPluginImages.DESC_OVR_WARNING);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see ILabelDecorator#decorateImage(Image, Object)
   */
  @Override
  public Image decorateImage(Image image, Object obj) {
    int adornmentFlags = computeAdornmentFlags(obj);
    if (adornmentFlags != 0) {
      ImageDescriptor baseImage = new ImageImageDescriptor(image);
      Rectangle bounds = image.getBounds();
      return getRegistry().get(
          new DartElementImageDescriptor(baseImage, adornmentFlags, new Point(
              bounds.width,
              bounds.height)));
    }
    return image;
  }

  /*
   * (non-Javadoc)
   * 
   * @see ILabelDecorator#decorateText(String, Object)
   */
  @Override
  public String decorateText(String text, Object element) {
    return text;
  }

  /*
   * (non-Javadoc)
   * 
   * @see IBaseLabelProvider#dispose()
   */
  @Override
  public void dispose() {
    if (fProblemChangedListener != null) {
      DartToolsPlugin.getDefault().getProblemMarkerManager().removeListener(fProblemChangedListener);
      fProblemChangedListener = null;
    }
    if (fRegistry != null && fUseNewRegistry) {
      fRegistry.dispose();
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see IBaseLabelProvider#isLabelProperty(Object, String)
   */
  @Override
  public boolean isLabelProperty(Object element, String property) {
    return true;
  }

  /*
   * (non-Javadoc)
   * 
   * @see IBaseLabelProvider#removeListener(ILabelProviderListener)
   */
  @Override
  public void removeListener(ILabelProviderListener listener) {
    if (fListeners != null) {
      fListeners.remove(listener);
      if (fListeners.isEmpty() && fProblemChangedListener != null) {
        DartToolsPlugin.getDefault().getProblemMarkerManager().removeListener(
            fProblemChangedListener);
        fProblemChangedListener = null;
      }
    }
  }

  /**
   * Note: This method is for internal use only. Clients should not call this method.
   * 
   * @param obj the element to compute the flags for
   * @return the adornment flags
   */
  protected int computeAdornmentFlags(Object obj) {
    try {
      if (obj instanceof IResource) {
        return getErrorTicksFromMarkers((IResource) obj, IResource.DEPTH_INFINITE, null);
      }
    } catch (CoreException e) {
      if (e instanceof DartModelException) {
        if (((DartModelException) e).isDoesNotExist()) {
          return 0;
        }
      }
      if (e.getStatus().getCode() == IResourceStatus.MARKER_NOT_FOUND) {
        return 0;
      }

      DartToolsPlugin.log(e);
    }
    return 0;
  }

  protected int getErrorTicksFromMarkers(IResource res, int depth, SourceReference sourceElement)
      throws CoreException {
    if (res == null || !res.isAccessible()) {
      return 0;
    }
    int severity = 0;
    try {
      if (sourceElement == null) {
        severity = res.findMaxProblemSeverity(IMarker.PROBLEM, true, depth);
      } else {
        IMarker[] markers = res.findMarkers(IMarker.PROBLEM, true, depth);
        if (markers != null && markers.length > 0) {
          for (int i = 0; i < markers.length && (severity != IMarker.SEVERITY_ERROR); i++) {
            IMarker curr = markers[i];
            if (isMarkerInRange(curr, sourceElement)) {
              int val = curr.getAttribute(IMarker.SEVERITY, -1);
              if (val == IMarker.SEVERITY_WARNING || val == IMarker.SEVERITY_ERROR) {
                severity = val;
              }
            }
          }
        }
      }

      if (severity == IMarker.SEVERITY_ERROR) {
        return ERRORTICK_ERROR;
      } else if (severity == IMarker.SEVERITY_WARNING) {
        return ERRORTICK_WARNING;
      }

      if (res instanceof IFolder) {
        IFolder folder = (IFolder) res;

        IPath path = folder.getLocation();

        if (path != null) {
          if (FileUtilities.isLinkedFile(path.toFile())) {
            return LINKED;
          }
        }
      }
    } catch (CoreException e) {
      // Ignored because if the resource has become unavailable we can simply not decorate it.
    }

    return 0;
  }

  /**
   * Tests if a position is inside the source range of an element.
   * 
   * @param pos Position to be tested.
   * @param sourceElement Source element (must be a DartElement)
   * @return boolean Return <code>true</code> if position is located inside the source element.
   * @throws CoreException Exception thrown if element range could not be accessed.
   */
  protected boolean isInside(int pos, SourceReference sourceElement) throws CoreException {
    if (fCachedRange == null) {
      fCachedRange = sourceElement.getSourceRange();
    }
    SourceRange range = fCachedRange;
    if (range != null) {
      int rangeOffset = range.getOffset();
      return (rangeOffset <= pos && rangeOffset + range.getLength() > pos);
    }
    return false;
  }

  private void fireProblemsChanged(IResource[] changedResources, boolean isMarkerChange) {
    if (fListeners != null && !fListeners.isEmpty()) {
      LabelProviderChangedEvent event = new ProblemsLabelChangedEvent(
          this,
          changedResources,
          isMarkerChange);
      Object[] listeners = fListeners.getListeners();
      for (int i = 0; i < listeners.length; i++) {
        ((ILabelProviderListener) listeners[i]).labelProviderChanged(event);
      }
    }
  }

  private int getErrorTicksFromAnnotationModel(IAnnotationModel model, SourceReference sourceElement)
      throws CoreException {
    int info = 0;
    Iterator<?> iter = model.getAnnotationIterator();
    while ((info != ERRORTICK_ERROR) && iter.hasNext()) {
      Annotation annot = (Annotation) iter.next();
      IMarker marker = isAnnotationInRange(model, annot, sourceElement);
      if (marker != null) {
        int priority = marker.getAttribute(IMarker.SEVERITY, -1);
        if (priority == IMarker.SEVERITY_WARNING) {
          info = ERRORTICK_WARNING;
        } else if (priority == IMarker.SEVERITY_ERROR) {
          info = ERRORTICK_ERROR;
        }
      }
    }
    return info;
  }

  private ImageDescriptorRegistry getRegistry() {
    if (fRegistry == null) {
      fRegistry = fUseNewRegistry ? new ImageDescriptorRegistry()
          : DartToolsPlugin.getImageDescriptorRegistry();
    }
    return fRegistry;
  }

  private IMarker isAnnotationInRange(IAnnotationModel model, Annotation annot,
      SourceReference sourceElement) throws CoreException {
    if (annot instanceof MarkerAnnotation) {
      if (sourceElement == null || isInside(model.getPosition(annot), sourceElement)) {
        IMarker marker = ((MarkerAnnotation) annot).getMarker();
        if (marker.exists() && marker.isSubtypeOf(IMarker.PROBLEM)) {
          return marker;
        }
      }
    }
    return null;
  }

  private boolean isInside(Position pos, SourceReference sourceElement) throws CoreException {
    return pos != null && isInside(pos.getOffset(), sourceElement);
  }

  private boolean isMarkerInRange(IMarker marker, SourceReference sourceElement)
      throws CoreException {
    if (marker.isSubtypeOf(IMarker.TEXT)) {
      int pos = marker.getAttribute(IMarker.CHAR_START, -1);
      return isInside(pos, sourceElement);
    }
    return false;
  }

}
